[모두의 메모] Framebuffer 전달 및 복사 - boostcampwm-2021/android05-boomerang GitHub Wiki
OpenGL ES의 렌더링 과정을 모두 마치면, EGLSurface에 Framebuffer가 들어가게 됩니다. 그런데 이 앱에서 Framebuffer가 필요한 Surface는 두 곳이 있습니다. 하나는 화면에 표시하기 위한 SurfaceView이고, 다른 하나는 동영상 저장을 위한 MediaCodec 입니다. 이 과정에서 제가 구현한 방법과 각 방법에 대해 실험한 결과에 대해 작성하겠습니다.
Surface 전달 방법
렌더링 두 번 하기
가장 먼저 떠오르는 방법은 각 Surface마다 렌더링을 해서 Framebuffer를 전달하는 것입니다.
eglCreateWindowSurface(...)는 Surface를 포함한 파라미터를 받습니다. SurfaceView와 MediaCodec이 전달한 Surface를 통해 EGLWindowSurface
를 생성합니다.
eglMakeCurrent(...) 함수를 사용하여 렌더링 대상이 될 EGLSurface를 선택할 수 있습니다.
이 방법은 SurfaceView의 Surface에 렌더링을 한 번 하고, 렌더링될 Surface를 바꾼 후 다시 렌더링을 합니다. 하나의 같은 프레임에 대해 렌더링을 두 번을 수행하기 때문에, 낭비라는 생각이 들 수 있습니다.
Framebuffer 복사
OpenGL ES 3.0 이상에서는 glBlitFramebuffer(...) 함수를 사용하여 Framebuffer간 특정 영역의 픽셀을 복사할 수 있습니다. 전체 영역을 복사한다면 렌더링을 다시 할 필요가 없을 것입니다.
(과정 코드, opengl 버전 확인 등)
fullFrameBlit.drawFrame(textureId, transformMatrix)
drawLine(currentPoint, height)
if (egl.glVersion == 3) {
// SurfaceView에 그릴 Framebuffer를 아직 swap하지 말고 인코딩 버퍼에 복사하고 둘 다 swap
encoderSurface.makeCurrentReadFrom(displaySurface)
GLES30.glBlitFramebuffer(
0,
0,
displaySurface.width,
displaySurface.height,
0,
0,
displaySurface.width,
displaySurface.height,
GLES30.GL_COLOR_BUFFER_BIT,
GLES30.GL_NEAREST
)
videoDoodleViewModel.encoder.transferBuffer()
encoderSurface.setPresentationTime(surfaceTexture.timestamp)
encoderSurface.swapBuffers()
displaySurface.makeCurrent()
displaySurface.swapBuffers()
} else {
// OpenGL ES 2.0일 경우. glBlitFramebuffer를 지원하지 않기에 그냥 두 번 그린다.
displaySurface.swapBuffers()
encoderSurface.makeCurrent()
GLES20.glViewport(0, 0, width, height)
fullFrameBlit.drawFrame(textureId, transformMatrix)
drawLine(currentPoint, height)
videoDoodleViewModel.encoder.transferBuffer()
encoderSurface.setPresentationTime(surfaceTexture.timestamp)
encoderSurface.swapBuffers()
displaySurface.makeCurrent()
}
이 과정을 위 코드로 구현하였습니다. glBlitFramebuffer(...)
을 사용할 수 없는 OpenGL ES 2.0 에서는 렌더링을 두 번 하는 방식을 그대로 쓰고, glBlitFramebuffer(...)
를 사용할 수 있는지 알기 위해 EGL 초기화 과정에서 가져온 OpenGL ES 버전에 따라 Framebuffer 전달 방식을 나누어 구현하였습니다.
이 방식에서는 렌더링을 한 번만 수행하고, 대신 glBlitFramebuffer(...)
과정이 추가되었습니다. 그렇다면 두 방법 중 어떤 방법이 더 빠른 속도로 작업을 마칠 수 있을까요?
각 방법의 초당 프레임 측정
이 측정을 하게 된 이유는 기존에는 위의 렌더링을 두 번 하는 방식만 사용하였는데, 프레임 저하 현상을 크게 느낄 수 있었기 때문입니다. 그래서 개선될 것으로 예상되는 방법을 찾고 비교해보았습니다.
두 번 렌더링하는 방식
30fps의 원본 동영상을 두 번 렌더링하는 기존 방식을 거쳐 다시 동영상으로 저장하였을 때, 사용자가 입력을 하지 않고 그대로 렌더링을 한다면 29.5fps로 원본과 큰 차이가 없었습니다. 하지만 사용자가 메모를 작성하였을 때에는 20.1fps로 큰 차이가 발생하였습니다. 한 번 렌더링 하는데 걸리는 시간이 길어지다보니 두 번 렌더링 하는 방식에서는 초당 프레임 감소가 더 심해졌던 것으로 추정됩니다.
Framebuffer 전달 방식 개선 결과
glBlitFramebuffer(...)
를 사용한 방법으로 변경 후 측정 결과입니다.
메모를 그리지 않고 동영상만 그대로 다시 렌더링하였을 때에는 오히려 개선 전보다 개선 후의 초당 프레임이 2 가량 낮아졌습니다.
하지만 동영상에 그림을 그리면서 렌더링을 한 결과는 변경 후 초당 프레임이 6 이상 높아졌습니다.
분석
측정 결과 모든 상황에서 복사가 렌더링보다 빠른 것은 아니었습니다.
위 그림은 측정 결과에 대해 저의 추정을 표현한 것입니다. 렌더링에 걸리는 시간은 그림을 그릴수록 더 길어질 것입니다. 그에 비해 glBlitFramebuffer(...)
의 실행 시간은 그림의 여부와는 상관없이 전체 픽셀을 복사하기 때문에 일정할 것입니다.
따라서 동영상을 그대로 다시 렌더링하였을 때에는 렌더링의 소요 시간이 glBlitFramebuffer(...)
보다 짧았던 것이고, 메모를 그렸을 때에는 렌더링의 소요 시간이 길어져 glBlitFramebuffer(...)
보다 길어졌기 때문에 위와 같은 결과가 나왔다고 생각됩니다.
다음 포스트에서는 MediaCodec에서 Surface를 사용하여 전달 받은 이미지 버퍼를 동영상으로 인코딩하는 과정에 대해 작성하겠습니다.